package org.chipu.shared.db.proxool;

import java.io.*;
import java.sql.*;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sql.DataSource;
import javax.xml.parsers.*;
import org.chipu.shared.interfaces.Loggeable;
import org.w3c.dom.*;
import org.xml.sax.SAXException;

public class ProxoolDataSource implements DataSource, Loggeable {
	private final Map<String, Properties> proxoolInfos = Collections.synchronizedMap(new HashMap<String, Properties>(2));
	private final Map<String, String> proxoolUrls = Collections.synchronizedMap(new HashMap<String, String>(2));
	private final String dbName;
	private Logger logger = Logger.getLogger(getClass().getName());

	private static final String aliasKey = "alias";
	private static final String driverURLKey = "driver_url";
	private static final String driverClassKey = "driver_class";
	private static final String propertiesKey = "properties";
	private static final String connectionCountKey = "proxool.maximum-connection-count";
	private static final String testSQLKey = "proxool.house-keeping-test-sql";
	private static final String userKey = "user";
	private static final String passwordKey = "password";
	private static final String statisticsKey = "proxool.statistics";

	public ProxoolDataSource(String path) throws ParserConfigurationException, SAXException, IOException, ClassNotFoundException {
		logger.info("Initializing proxool driver");
		Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");

		Map<String, ?> settings = new Configurator(path).parse();
		dbName = (String) settings.get(aliasKey);
		proxoolInfos.put(dbName, (Properties) settings.get(propertiesKey));
		proxoolUrls.put(dbName, "proxool." + dbName + ":" + settings.get(driverClassKey) + ":" + settings.get(driverURLKey) + "/"
			+ dbName);
	}

	/** MySQL database is hard-coded */
	public ProxoolDataSource(String dbName, String mysqlServer, int mysqlPort, String user, String pass, int maxConnections)
		throws ClassNotFoundException {
		this("com.mysql.jdbc.Driver", "jdbc:mysql", dbName, mysqlServer, mysqlPort, user, pass, maxConnections);
	}

	public ProxoolDataSource(String driverClassname, String driverProtocol, String dbName, String dbServer, int dbPort,
		String user, String pass, int maxConnections) throws ClassNotFoundException {
		Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
		this.dbName = dbName;
		Properties proxoolInfo = new Properties();
		proxoolInfo.setProperty(connectionCountKey, String.valueOf(maxConnections));
		proxoolInfo.setProperty(testSQLKey, "select CURRENT_DATE");
		proxoolInfo.setProperty(userKey, user);
		proxoolInfo.setProperty(passwordKey, pass);
		proxoolInfo.setProperty(statisticsKey, "10m,1h,1d");
		proxoolInfos.put(dbName, proxoolInfo);
		proxoolUrls.put(dbName, "proxool." + dbName + ":" + driverClassname + ":" + driverProtocol + "://" + dbServer + ":"
			+ dbPort + "/" + dbName);
	}

	@Override
	public void setLogger(Logger logger) {
		if (logger != null)
			this.logger = logger;
	}

	private Connection getConnection(String dbName) throws SQLException {
		try {
			return DriverManager.getConnection(proxoolUrls.get(dbName), proxoolInfos.get(dbName));
		} catch(SQLException ex) {
			logger.severe("Error creating db connection: " + dbName);
			throw ex;
		}
	}

	@Override
	public Connection getConnection() throws SQLException {
		return getConnection(dbName);
	}

	@Override
	public Connection getConnection(String username, String password) throws SQLException {
		return getConnection(dbName);
	}

	public void freeConnection(Connection conn) {
		try {
			conn.close();
		} catch(SQLException ex) {
			logger.log(Level.SEVERE, "Error closing connection", ex);
		}
	}

	@Override
	public PrintWriter getLogWriter() {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public int getLoginTimeout() {
		return 0;
	}

	@Override
	public void setLogWriter(PrintWriter out) {
		// TODO Auto-generated method stub

	}

	@Override
	public void setLoginTimeout(int seconds) {
		// TODO Auto-generated method stub

	}

	@Override
	public boolean isWrapperFor(Class<?> iface) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public <T> T unwrap(Class<T> iface) {
		// TODO Auto-generated method stub
		return null;
	}

	/**
	 * The class handling configuration for ProxoolDataSource
	 * @author xin
	 */
	@SuppressWarnings("unchecked")
	private static class Configurator {
		private Document xmlDoc = null;
		private final Map settings = new HashMap();

		/** A set of xml element names */
		private final static String PROXOOL = "proxool";
		private final static String ALIAS = "alias";
		private final static String DRIVER_URL = "driver-url";
		private final static String DRIVER_CLASS = "driver-class";
		private final static String DRIVER_PROPERTIES = "driver-properties";
		private final static String PROPERTY = "property";
		private final static String MAX_CON = "maximum-connection-count";
		private final static String HOUSE_KEEP_SQL = "house-keeping-test-sql";

		/** A set of xml attribute names */
		private final static String NAME_ATTR = "name";
		private final static String VALUE_ATTR = "value";

		Configurator(String path) throws ParserConfigurationException, SAXException, IOException {
			this(new File(path));
		}

		Configurator(File config) throws ParserConfigurationException, SAXException, IOException {
			DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			xmlDoc = builder.parse(config);
		}

		Map<String, ?> parse() {
			if (xmlDoc != null) {
				// The xml config setting for this connector might be embeded in other xml config setting
				// Thus, only setting inside <proxool> ... </proxool> are specific to this connector
				NodeList list = xmlDoc.getDocumentElement().getElementsByTagName(PROXOOL);
				if (list.getLength() == 0)
					return null;
				Element element = (Element) list.item(0);
				settings.put(aliasKey, getTextContents(element, ALIAS));
				settings.put(driverURLKey, getTextContents(element, DRIVER_URL));
				settings.put(driverClassKey, getTextContents(element, DRIVER_CLASS));
				Properties properties = loadDriverProperties((Element) element.getElementsByTagName(DRIVER_PROPERTIES).item(0));
				properties.setProperty(connectionCountKey, getTextContents(element, MAX_CON));
				properties.setProperty(testSQLKey, getTextContents(element, HOUSE_KEEP_SQL));
				settings.put(propertiesKey, properties);
				xmlDoc = null;
			}
			return settings;
		}

		private Properties loadDriverProperties(Element props) {
			NodeList list = props.getElementsByTagName(PROPERTY);
			Properties properties = new Properties();
			for(int i = 0; i < list.getLength(); i++) {
				Element property = (Element) list.item(i);
				String key = property.getAttribute(NAME_ATTR);
				String value = property.getAttribute(VALUE_ATTR);
				properties.put(key, value);
			}
			return properties;
		}

		private static String getTextContents(Element element, String tagname) {
			return getTextContents(element.getElementsByTagName(tagname).item(0));
		}

		private static String getTextContents(Node node) {
			checkNull(node, "node");
			NodeList childNodes;
			StringBuffer contents = new StringBuffer();

			childNodes = node.getChildNodes();
			for(int i = 0; i < childNodes.getLength(); i++)
				if (childNodes.item(i).getNodeType() == Node.TEXT_NODE)
					contents.append(childNodes.item(i).getNodeValue());
			return contents.toString();
		}

		public static Element[] getDirectElementsByTagName(Node node, String name) {
			checkNull(node, "node");
			checkString(name, "name");
			NodeList childNodes = node.getChildNodes();
			List<Element> list = new ArrayList<Element>();
			for(int i = 0; i < childNodes.getLength(); i++)
				if (childNodes.item(i).getNodeType() == Node.ELEMENT_NODE) {
					Element element = (Element) childNodes.item(i);
					if (element.getTagName().equals(name))
						list.add(element);
				}
			Element[] elements = new Element[list.size()];
			int i = 0;
			for(Element element : list)
				elements[i++] = element;
			return elements;
		}

		/**
		 * Checks whether the given Object is null.
		 * @param arg the argument to check
		 * @param name the name of the argument to check
		 * @throws IllegalArgumentException if the given Object is null
		 */
		private static void checkNull(Object arg, String name) {
			if (arg == null)
				throw new NullPointerException(name + " should not be null.");
		}

		/**
		 * Checks whether the given String is null or empty.
		 * @param arg the String to check
		 * @param name the name of the String argument to check
		 * @throws IllegalArgumentException if the given string is empty or null
		 */
		private static void checkString(String arg, String name) {
			checkNull(arg, name);
			if (arg.trim().length() == 0)
				throw new IllegalArgumentException(name + " should not be empty.");
		}
	}

	public String getUser() {
		return proxoolInfos.get(dbName).getProperty("user");
	}

	public String getPassword() {
		return proxoolInfos.get(dbName).getProperty("password");
	}

	public String getDB() {
		String url = proxoolUrls.get(dbName);
		return url.substring(8, url.indexOf(':'));
	}
}